package com.black.play.entity;

import com.black.play.component.SnakeWindow;
import com.black.play.config.AppConfig;
import com.black.play.constant.AppConstant;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.RoundRectangle2D;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

/**
 * 蛇类 关键类
 * 继承JPanel 用于定位所有Food节点对象
 * 维护蛇身节点链表
 *
 * @author ylx
 */
public abstract class Snake extends JPanel
{
    // 蛇头坐标x
    protected int headX;
    // 蛇头坐标y
    protected int headY;
    // 蛇身长度
    protected int length;
    // 蛇身节点链表
    protected LinkedList<Food> body;
    // 下一食物
    protected Food food;
    // 蛇移动方向
    protected Direction direction;
    // 蛇移动定时器
    protected Timer timer;
    public int[][] offsets;
    public static final int WIDTH = AppConfig.getInt("window.width");
    public static final int HEIGHT = AppConfig.getInt("window.height");
    public static final int INIT_LENGTH = AppConfig.getOrDefault("snake.init.length", 4);
    public static final int MAX_DELAY = AppConfig.getInt("snake.move.max.delay");
    public static final int MIN_DELAY = AppConfig.getInt("snake.move.min.delay");
    public static final Random RANDOM = new Random();
    // 匿名内部类加载配置
    public static final Dimension NODE_SIZE = new Dimension(20, 20)
    {
        {
            int width = AppConfig.getInt("snake.node.width");
            int height = AppConfig.getInt("snake.node.height");
            if (width != 0 && height != 0)
            {
                this.width = width;
                this.height = height;
            }
            // 获取网格大小 该配置会覆盖节点大小
            // 若不使用改配置  则空配置或配置0
            int numX = AppConfig.getInt("snake.grid.numX");
            int numY = AppConfig.getInt("snake.grid.numY");
            if (numX != 0 && numY != 0)
            {
                this.width = WIDTH / numX;
                this.height = HEIGHT / numY;
            }
        }
    };

    /**
     * 实例化时构造必要参数
     */
    public Snake()
    {
        // 初始化蛇身节点链表
        this.body = new LinkedList<>();
        // 初始化移动定时器
        this.timer = new Timer(MAX_DELAY, this::move);
        // 不适用JavaSwing布局管理器 采用(x,y)坐标定位布局
        // 默认布局管理器是FlowLayout
        // 坐标x→递增  y↓递增 窗口左上角为原点(0,0)
        this.setLayout(null);
        // 设置画板大小铺满窗口
        this.setPreferredSize(new Dimension(WIDTH, HEIGHT));
        this.setBackground(SnakeWindow.BACKGROUND);
    }

    /**
     * 开始游戏时的初始化内容
     */
    public void init()
    {
        // 初始化蛇身长度 由配置决定
        length = INIT_LENGTH;
        if (length <= 0)
        {
            throw new RuntimeException(AppConstant.INIT_ERROR);
        }
        // 初始化更新窗口内容
        SnakeWindow instance = SnakeWindow.getInstance();
        instance.updateLength(length);
        instance.updateScore(0);
        instance.updateSpeed((double) 1000 / MAX_DELAY);
        // 清空蛇体列表
        body.clear();
        // 随机初始化蛇头坐标
        Food head = randomFood();
        headX = head.getX();
        headY = head.getY();
        // 设置头颜色并添加到蛇身第一个元素
        head.setColor(Food.HEAD_COLOR);
        body.add(head);
        // 随机方向继续初始化蛇体
        Direction[] directions = Direction.values();
        init(head, directions, 1);
        // 随机初始化下一食物
        food = randomFood();
    }

    /**
     * 随机初始化下一食物
     * 直接对列表深拷贝过于费时
     * 找到后对单独的进行拷贝
     *
     * @return 随机节点
     */
    protected abstract Food randomFood();

    /**
     * 根据当前节点 随机方向初始化下一节点
     * 并递归调用直到蛇长满足初始化设置
     *
     * @param food       当前节点
     * @param directions 方向值
     */
    protected abstract void init(Food food, Direction[] directions, int length);

    /**
     * 在给定集合中 根据当前节点和方向 看集合中是否有该方向上的邻居节点
     *
     * @param foods     节点列表  (传入除蛇身外全节点列表时可用于 生成平面连续的节点列表; 传入body时可以用于检测碰撞自身)
     * @param food      当前节点
     * @param direction 指定方向
     * @return 该节点该方向存在节点返回该节点, 否则返回null
     */
    public abstract Food getNextByDirection(List<Food> foods, Food food, Direction direction);

    public abstract Direction getDirectionByNext();

    /**
     * 暂停事件恢复移动||首次开始按键开始移动
     */
    public void start()
    {
        timer.start();
    }

    /**
     * 暂停点击回主菜单结束||死亡结束
     */
    public void stop()
    {
        timer.stop();
        SnakeWindow.getInstance().showCard(AppConstant.MENU);
    }

    /**
     * 空格或Esc暂停
     */
    public void pause()
    {
        timer.stop();
    }

    /**
     * 键盘监听调用
     *
     * @param direction 按键输入方向
     */
    public void changeDirection(Direction direction)
    {
        if (body.size() == 1)
        {
            this.direction = direction;
            move();
            start();
            return;
        }
        // 获取蛇头和蛇身第二节
        Food head = body.getFirst();
        Food second = body.get(1);
        // 获取该方向下一节点
        Food next = getNextByDirection(body, head, direction);
        // 如果第二节点和该方向下一节点不同 则改变方向并移动一次
        // 否则不做改变 即不允许180°调头走
        if (!second.touchTo(next))
        {
            this.direction = direction;
            move();
        }
        // 到此没有对方向赋值说明是起步就进行180°掉头
        // 赋予反向启动方向
        if (this.direction == null)
        {
            this.direction = direction.reverse();
        }
        // 启动定时器
        if (!timer.isRunning())
        {
            start();
        }
        SnakeWindow.getInstance().start();
    }

    /**
     * 移动
     *
     * @param e 触发事件
     */
    public synchronized void move(ActionEvent... e)
    {
        // 根据方向移动蛇头坐标
        int ordinal = direction.ordinal();
        int[] offset = offsets[ordinal];
        headX += offset[0];
        headY += offset[1];
        // 根据判断是否碰壁或自身
        if (touchSelfOrWall())
        {
            stop();
        }
        // 判断是否碰到食物
        if (touchFood())
        {
            addBody();
        }
        else
        {
            moveBody();
        }
        updateUI();
    }

    /**
     * 碰撞墙体或自身检测
     *
     * @return 是否碰撞
     */
    protected boolean touchSelfOrWall()
    {
        // 获取蛇头和尾部
        Food head = body.getFirst();
        Food last = body.getLast();
        // 根据头部和移动方向 获取该方向在蛇体上的相邻点
        Food nextOnBody = getNextByDirection(body, head, direction);
        // 如果该方向在蛇体上存在节点 并且 不是尾部节点 则说明吃到自身 判定死亡
        if (nextOnBody != null)
        {
            if (nextOnBody.equals(last))
            {
                Direction lastDirection = getDirectionByNext();
                int ordinal = lastDirection.ordinal();
                int[] offset = offsets[ordinal];
                Food expectLast = new Food(last.getX() + offset[0], last.getY() + offset[1]);
                Food expectHead = new Food(headX, headY);
                return expectHead.touchTo(expectLast);
            }
            return true;
        }
        // 否则判定蛇头坐标是否会超出边界
        return headX < 0 || headX >= WIDTH - NODE_SIZE.width || headY < 0 || headY >= HEIGHT - NODE_SIZE.height;
    }

    /**
     * 接触食物
     *
     * @return 是否接触到食物
     */
    protected boolean touchFood()
    {
        // 期望中移动后的蛇头与食物是否接触
        Food expectHead = new Food(headX, headY);
        return expectHead.touchTo(food);
    }

    protected abstract void addBody();

    protected abstract void moveBody();

    /**
     * 重写绘制方法
     *
     * @param g the <code>Graphics</code> 画笔
     */
    @Override
    protected void paintComponent(Graphics g)
    {
        // 父类设定边界大小 绘制背景等
        super.paintComponent(g);
        Graphics2D graphics2D = (Graphics2D) g;
        // 设定边框颜色 绘制圆角边框
        graphics2D.setColor(Color.WHITE);
        RoundRectangle2D.Float border = new RoundRectangle2D.Float(this.food.getX(), this.food.getY(), NODE_SIZE.width, NODE_SIZE.height, NODE_SIZE.width >> 2, NODE_SIZE.height >> 2);
        graphics2D.draw(border);
        // 设置食物颜色填充
        graphics2D.setColor(this.food.getColor());
        graphics2D.fill(border);
        // 同理绘制身体
        for (Food food : body)
        {
            RoundRectangle2D.Float bodyBorder = new RoundRectangle2D.Float(food.getX(), food.getY(), NODE_SIZE.width, NODE_SIZE.height, NODE_SIZE.width >> 2, NODE_SIZE.height >> 2);
            graphics2D.setColor(Color.WHITE);
            graphics2D.draw(bodyBorder);
            graphics2D.setColor(food.getColor());
            graphics2D.fill(bodyBorder);
        }
    }
}
